fix(rsc): handle export * re-exports in use client and use server modules#1234
Conversation
The `use-client` proxy transform threw `unsupported ExportAllDeclaration` on any `"use client"` file containing `export * from '...'`. Pre-resolve bare `export *` declarations to explicit named re-exports before running the proxy transform (mirroring Next.js's webpack plugin behavior), and handle `export * as ns from` directly in the pure proxy/wrap transforms since the name is statically known. Closes cloudflare/vinext#1352
hi-ogawa
left a comment
There was a problem hiding this comment.
Thanks for working on this. The idea makes sense to me.
hi-ogawa
left a comment
There was a problem hiding this comment.
Thanks for the follow-up. I reviewed this again, and it looks good to merge.
I left a few comments for things I might clean up separately on my side. Mainly I want to extract expandExportAllDeclarations into a pure-ish utility so we can unit test it directly. There are also a few edge cases I want to tweak for export name conflicts and load/parse failure mode. In case interested, here is my follow-up note: https://gist.github.com/hi-ogawa/cf434aff19cc869e6cfae366851db0c7
| return names | ||
| } | ||
|
|
||
| async function expandExportAllDeclarations( |
There was a problem hiding this comment.
probably it's better to extract this as src/transforms/ util with unit test.
| 'use client' | ||
|
|
||
| export * from './named' |
There was a problem hiding this comment.
coverage for "use server" too
| try { | ||
| const raw = await fs.promises.readFile(resolvedId, 'utf-8') | ||
| moduleCode = await transformSourceForExportScan(raw, resolvedId) | ||
| } catch { | ||
| return [] | ||
| } | ||
| if (!moduleCode) return [] | ||
|
|
||
| let ast: Awaited<ReturnType<typeof parseAstAsync>> | ||
| try { | ||
| ast = await parseAstAsync(moduleCode) | ||
| } catch { | ||
| return [] | ||
| } |
There was a problem hiding this comment.
we can start from surfacing these errors to start with so it's easier to iterate on unsupported case.
export * re-exports in use client modulesexport * re-exports in use client and use server modules
| throw Object.assign(new Error('unsupported ExportAllDeclaration'), { | ||
| pos: node.start, | ||
| }) | ||
| if (node.type === 'ExportAllDeclaration') { |
There was a problem hiding this comment.
also, this changes looks unneeded as it cannot be useful for server function case at least.
Summary
The
use-clientproxy transform in@vitejs/plugin-rscthrowsunsupported ExportAllDeclarationwhenever a"use client"file containsexport * from '...'. Reported downstream in cloudflare/vinext#1352, where Next.js's app-dir test suite hits this with a real-worldcomponents/export-all/index.jsfixture.fixes #1233
Reproduction
A new fixture under
packages/plugin-rsc/examples/basic/src/routes/export-all/reproduces the original error onmain:It builds cleanly with the fix.
Fix
Pure transforms (
proxy-export.ts,wrap-export.ts) now handle theexport * as ns from '...'form directly, since the namespace name is statically known.Plugin layer (
plugin.ts) introduces anexpandExportAllDeclarationshelper used by bothvitePluginUseClientandvitePluginUseServer. For each bareexport * from '...'declaration, it resolves the source, recursively collects the named exports via AST walk, and rewrites the source toexport { a, b, c } from '...'before the proxy transform runs. This mirrors what Next.js's webpack plugin does to pre-resolveexport *into named exports.this.load({ id }).code(which Rollup populates).ModuleInfo.codeis unsupported andtransformRequestreturns module-runner output (__vite_ssr_exportName__(...)) that lacks readable ESM exports, so it falls back tofs.readFile+transformWithOxcto get standard JS the AST walk can read.Test plan
proxy-export.test.tsforexport * as ns from, the post-resolution form, theignoreExportAllDeclarationescape hatch, and the still-thrown unresolved-bare case; updatedwrap-export.test.tssnapshot. All 428 unit tests pass.export *inbasic.test.tsagainst the new fixture. All 151 basic e2e tests pass in both dev and build modes.main(stashed the fix, rebuilt, build fails with the exact error from the issue) and disappears with the fix.